title: git文档10 - Git 内部原理
date: 2020.11.15 # 瞎扯
top:

categories:

tags:


10.1 底层命令与上层命令
10.2 Git 对象
10.3 Git 引用
10.4 包文件
10.5 引用规范
10.6 传输协议
10.7 维护与数据恢复
10.8 环境变量
10.9 总结

2020. 星期 :

底层命令与上层命令

从根本上来讲 Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面。

底层命令与上层命令

本书主要涵盖了 checkout、branch、remote 等约 30 个 Git 的子命令。 这部分命令一般被称作“底层(plumbing)”命令,而那些更友好的命令则被称作“上层(porcelain)”命令。

新初始化的 .git 目录的典型结构如下:

$ ls -F1
config
description
HEAD
hooks/
info/
objects/
  |- fa/38gezifu(40-2)
  |- infos/
    |- packs/
  |- pack/
  |- ORIG_HEAD
  |- packed-refs
refs/
  |- heads/
    |- 本地的分支| dev-feature-some-20201030-updateSth
  |- remotes/
    |- origin/  
  |-tags/
  |- stash

随着 Git 版本的不同,该目录下可能还会包含其他内容。 不过对于一个全新的 git init 版本库,这将是你看到的默认结构。
description 文件仅供 GitWeb 程序使用,我们无需关心。
config 文件包含项目特有的配置选项。
info 目录包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在 .gitignore 文件中的忽略模式(ignored patterns)。
hooks 目录包含客户端或服务端的钩子脚本(hook scripts), 在 Git 钩子 中这部分话题已被详细探讨过。

剩下的四个条目很重要:HEAD 文件、(尚待创建的)index 文件,和 objects 目录、refs 目录。 它们都是 Git 的核心组成部分。
objects 目录存储所有数据内容;
refs 目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针;
HEAD 文件指向目前被检出的分支;
index 文件保存暂存区信息。

10.2 Git 对象

Git 对象

Git 是一个内容寻址文件系统

树对象(tree object)

它能解决文件名保存的问题,也允许我们将多个文件组织到一起。

一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针,以及相应的模式、类型、文件名信息。

$ git cat-file -p master^{tree}
100644 blob a906cb2a4a904a152e80877d4088654daad0c859      README
100644 blob 8f94139338f9404f26296befa88755fc2598c289      Rakefile
040000 tree 99f1a6d12cb4b6f19c8655fca46c3ecf317074e0      lib

master^{tree} 语法表示 master 分支上最新的提交所指向的树对象。

Figure 149. 简化版的 Git 数据模型。

因此,为创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。 可以通过底层命令 git update-index 为一个单独文件——我们的 test.txt 文件的首个版本——创建一个暂存区。

我们指定的文件模式为 100644,表明这是一个普通文件。 其他选择包括:100755,表示一个可执行文件;120000,表示一个符号链接。 这里的文件模式参考了常见的 UNIX 文件模式,但远没那么灵活——上述三种模式即是 Git 文件(即数据对象)的所有合法模式(当然,还有其他一些模式,但用于目录项和子模块)。

这就是每次我们运行 git add 和 git commit 命令时,Git 所做的工作实质就是将被改写的文件保存为数据对象, 更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。 这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects 目录下。

Figure 151. 你的 Git 目录下所有可达的对象。

对象存储

Git 首先会以识别出的对象的类型作为开头来构造一个头部信息,本例中是一个“blob”字符串。 接着 Git 会在头部的第一部分添加一个空格,随后是数据内容的字节数,最后是一个空字节(null byte)

就是这样——你已创建了一个有效的 Git 数据对象。

所有的 Git 对象均以这种方式存储,区别仅在于类型标识——另两种对象类型的头部信息以字符串“commit”或“tree”开头,而不是“blob”。
另外,虽然数据对象的内容几乎可以是任何东西,但提交对象和树对象的内容却有各自固定的格式。

10.3 Git 内部原理 - Git 引用

Git 引用

不过你需要记得 1a410e 是你查看历史的起点提交。 如果我们有一个文件来保存 SHA-1 值,而该文件有一个简单的名字, 然后用这个名字指针来替代原始的 SHA-1 值的话会更加简单。

在 Git 中,这种简单的名字被称为“引用(references,或简写为 refs)”。

Figure 152. 包含分支引用的 Git 目录对象。
当运行类似于 git branch <branch> 这样的命令时,Git 实际上会运行 update-ref 命令, 取得当前所在分支最新提交对应的 SHA-1 值,并将其加入你想要创建的任何新引用中。

HEAD 引用

现在的问题是,当你执行 git branch 时,Git 如何知道最新提交的 SHA-1 值呢? 答案是 HEAD 文件。
HEAD 文件通常是一个符号引用(symbolic reference),指向目前所在的分支。 所谓符号引用,表示它是一个指向其他引用的指针。

当我们执行 git commit 时,该命令会创建一个提交对象,并用 HEAD 文件中那个引用所指向的 SHA-1 值设置其父提交字段。

标签引用

标签对象(tag object) 非常类似于一个提交对象——它包含一个标签创建者信息、一个日期、一段注释信息,以及一个指针。
标签对象通常指向一个提交对象,而不是一个树对象。 它像是一个永不移动的分支引用——永远指向同一个提交对象,只不过给这个提交对象加上一个更友好的名字罢了。

$ git tag -a v1.1 1a410efbd13591db07496601ebc7a059dd55cfe9 -m 'test tag'

$ cat .git/refs/tags/v1.1
9585191f37f7b0fb9444f35a9bf50de191beadc2

# 现在对该 SHA-1 值运行 git cat-file -p 命令:
$ git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
object 1a410efbd13591db07496601ebc7a059dd55cfe9
type commit
tag v1.1
tagger Scott Chacon <schacon@gmail.com> Sat May 23 16:48:58 2009 -0700

test tag

我们注意到,object 条目指向我们打了标签的那个提交对象的 SHA-1 值。 另外要注意的是,标签对象并非必须指向某个提交对象;你可以对任意类型的 Git 对象打标签。

远程引用 (remote reference)

,并保存在 refs/remotes 目录下。

10.4 Git 内部原理 - 包文件

包文件

这意味着,虽然你只是在一个 400 行的文件后面加入一行新内容,Git 也会用一个全新的对象来存储新的文件内容:

Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。 当版本库中有太多的松散对象,或者你手动执行 git gc 命令,或者你向远程服务器执行推送时,Git 都会这样做。 要看到打包过程,

剩下的文件是新创建的包文件和一个索引
包文件包含了刚才从文件系统中移除的所有对象的内容。
索引文件包含了包文件的偏移信息,我们通过索引文件就可以快速定位任意一个指定对象。

Git 是如何做到这点的? Git 打包对象时,会查找命名及大小相近的文件,并只保存文件不同版本之间的差异内容。 你可以查看包文件,观察它是如何节省空间的。 git verify-pack 这个底层命令可以让你查看已打包的内容:

同样有趣的地方在于,第二个版本完整保存了文件内容,而原始的版本反而是以差异方式保存的——这是因为大部分情况下需要快速访问文件的最新版本。

最妙之处是你可以随时重新打包。 Git 时常会自动对仓库进行重新打包以节省空间。当然你也可以随时手动执行 git gc 命令来这么做。

10.5 引用规范(refspec)

$ git remote add origin https://github.com/schacon/simplegit-progit
运行上述命令会在你仓库中的 .git/config 文件中添加一个小节, 并在其中指定远程版本库的名称(origin)、URL 和一个用于获取操作的 引用规范:

[remote "origin"]
    url = https://github.com/schacon/simplegit-progit
    fetch = +refs/heads/*:refs/remotes/origin/*
  ## 让 Git 每次只拉取远程的 master 分支,而不是所有分支
  # fetch = +refs/heads/master:refs/remotes/origin/master

  ## 你也可以在配置文件中指定多个用于获取操作的引用规范。
    ## 远程仓库获取时都包括 master 和 experiment 分支
  fetch = +refs/heads/master:refs/remotes/origin/master
    fetch = +refs/heads/experiment:refs/remotes/origin/experiment
  ## 命名空间(或目录)
  fetch = +refs/heads/master:refs/remotes/origin/master
    fetch = +refs/heads/qa/*:refs/remotes/origin/qa/*


  # 引用规范推送 `git push origin master:refs/heads/qa/master`
  ## Git 每次运行 `git push origin` 时都像上面这样推送
  push = refs/heads/master:refs/heads/qa/master

引用规范的格式由一个可选的 + 号和紧随其后的 <src>:<dst> 组成,
其中 <src> 是一个模式(pattern),代表远程版本库中的引用;
<dst> 是本地跟踪的远程引用的位置。
+ 号告诉 Git 即使在不能快进的情况下也要(强制)更新引用。

1) 如果服务器上有一个 master 分支,你可以在本地通过下面任意一种方式来访问该分支上的提交记录:

$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master

上面的三个命令作用相同,因为 Git 会把它们都扩展成 refs/remotes/origin/master。

1) 如果有某些只希望被执行一次的操作,我们也可以在命令行指定引用规范。

git fetch origin master:refs/remotes/origin/mymaster

1) 你也可以指定多个引用规范。 在命令行中,你可以按照如下的方式拉取多个分支:

$ git fetch origin master:refs/remotes/origin/mymaster \ 
  topic:refs/remotes/origin/topic

我们不能在模式中使用部分通配符
1) 但我们可以使用命名空间(或目录)来达到类似目的

### 引用规范推送
$ git push origin master:refs/heads/qa/master

### 删除引用
$ git push origin :topic
因为引用规范(的格式)是 <src>:<dst>,所以上述命令把 <src> 留空,意味着把远程版本库的 topic 分支定义为空值,也就是删除它。
$ git push origin --delete topic

10.6 传输协议

Git 可以通过两种主要的方式在版本库之间传输数据:“哑(dumb)”协议和“智能(smart)”协议。

哑协议: 一个基于 HTTP 协议的只读版本库
在传输过程中,服务端不需要有针对 Git 特有的代码;抓取过程是一系列 HTTP 的 GET 请求,这种情况下,客户端可以推断出服务端 Git 仓库的布局。

智能协议
哑协议虽然很简单但效率略低,且它不能从客户端向服务端发送数据。
智能协议是更常用的传送数据的方法,但它需要在服务端运行一个进程,而这也是 Git 的智能之处——它可以读取本地数据,理解客户端有什么和需要什么,并为它生成合适的包文件。 总共有两组进程用于传输数据,它们分别负责上传和下载数据。

上传数据

为了上传数据至远端,Git 使用 send-pack 和 receive-pack 进程。 运行在客户端上的 send-pack 进程连接到远端运行的 receive-pack 进程。

举例来说,在项目中使用命令 git push origin master 时, origin 是由基于 SSH 协议的 URL 所定义的。 Git 会运行 send-pack 进程,它会通过 SSH 连接你的服务器。 它会尝试通过 SSH 在服务端执行命令,就像这样:

下载数据

当你在下载数据时, fetch-pack 和 upload-pack 进程就起作用了。 客户端启动 fetch-pack 进程,连接至远端的 upload-pack 进程,以协商后续传输的数据。

10.7 维护与数据恢复

维护

Git 会不定时地自动运行一个叫做 “auto gc” 的命令。
“gc” 代表垃圾回收,这个命令会做以下事情:收集所有松散对象并将它们放置到包文件中, 将多个包文件合并为一个大的包文件,移除与任何提交都不相关的陈旧对象。
$ git gc --auto
这个命令通常并不会产生效果。 大约需要 7000 个以上的松散对象或超过 50 个的包文件才能让 Git 启动一次真正的 gc 命令。
你可以通过修改 gc.auto 与 gc.autopacklimit 的设置来改动这些数值。

gc 将会做的另一件事是打包你的引用到一个单独的文件。
为了保证效率 Git 会将它们移动到名为 .git/packed-refs 的文件中,
Git 会首先在 refs 目录中查找指定的引用,然后再到 packed-refs 文件中查找。

数据恢复

最方便,也是最常用的方法,是使用一个名叫 git reflog 的工具。
当你正在工作时,Git 会默默地记录每一次你改变 HEAD 时它的值。 每一次你提交或改变分支,引用日志都会被更新。

你可以通过创建一个新的分支指向这个提交来恢复它
git branch recover-branch ab1afef
git log --pretty=oneline recover-branch

由于引用日志数据存放在 .git/logs/ 目录中,现在你已经没有引用日志了。
一种方式是使用 git fsck 实用工具,将会检查数据库的完整性。
如果使用一个 –full 选项运行它,它会向你显示出所有没有被其他对象指向的对象:
$_PS: stash 的丢失,也可以通过这个指令找出/恢复。----lost-found : write dangling objects in .git/lost-found

移除对象

当你迁移 Subversion 或 Perforce 仓库到 Git 的时候,这会是一个严重的问题

## 错误添加
$ curl https://www.kernel.org/pub/software/scm/git/git-2.1.0.tar.gz > git.tgz
$ git add git.tgz
$ git commit -m 'add git tarball'
## 其实这个项目并不需要这个巨大的压缩文件。 现在我们将它移除
git rm git.tgz
## 执行 gc 来查看数据库占用了多少空间
$ git gc
### 你也可以执行 count-objects 命令来快速的查看占用空间大小
git count-objects -v

size-pack 的数值指的是你的包文件以 KB 为单位计算的大小,所以你大约占用了 5MB 的空间。
在最后一次提交前,使用了不到 2KB ——显然,从之前的提交中移除文件并不能从历史中移除它。

首先你必须找到它。
但是假设你不知道;你可以通过运行 git verify-pack 命令, 然后对输出内容的第三列(即文件大小)进行排序,从而找出这个大文件。
你也可以将这个命令的执行结果通过管道传送给 tail 命令,因为你只需要找到列在最后的几个大对象。

$ git filter-branch --index-filter \
  'git rm --ignore-unmatch --cached git.tgz' -- 7b30847^..

## 你的历史中将不再包含对那个文件的引用。 不过,你的引用日志和你在 .git/refs/original 通过
$ rm -Rf .git/refs/original
$ rm -Rf .git/logs/
$ git gc
## 让我们看看你省了多少空间。
$ git count-objects -v
## 这个大文件还在你的松散对象中,并没有消失;
## 如果真的想要删除它,可以通过有 --expire 选项的 git prune 命令来完全地移除那个对象
$ git prune --expire now
$ git count-objects -v

10.8 环境变量

Git 总是在一个 bash shell 中运行,并借助一些 shell 环境变量来决定它的运行方式。

全局行为
GIT_EXEC_PATH
GIT_EDITOR

版本库位置
GIT_DIR
GIT_WORK_TREE
GIT_OBJECT_DIRECTORY

路径规则
GIT_GLOB_PATHSPECS 和 GIT_NOGLOB_PATHSPECS 控制通配符在路径规则中的默认行为。
GIT_LITERAL_PATHSPECS
GIT_ICASE_PATHSPECS

提交
GIT_AUTHOR_NAME, _EMAIL, _DATE
GIT_COMMITTER_NAME, .. , ..

网络
Git 使用 curl 库通过 HTTP 来完成网络操作, 所以 GIT_CURL_VERBOSE 告诉 Git 显示所有由那个库产生的消息。
GIT_SSL_NO_VERIFY
GIT_HTTP_LOW_SPEED_TIME
GIT_HTTP_USER_AGENT

比较和合并
GIT_DIFF_OPTS
GIT_EXTERNAL_DIFF
GIT_MERGE_VERBOSITY

调试
GIT_TRACE